package sto.orz.lang.text;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.apache.commons.lang.text.StrBuilder;
import org.apache.commons.lang.text.StrMatcher;

public class StrHoldTokenTokenizer extends
		org.apache.commons.lang.text.StrTokenizer {
	public StrHoldTokenTokenizer() {
		super();
	}

	public StrHoldTokenTokenizer(char[] input, char delim, char quote) {
		super(input, delim, quote);
	}

	public StrHoldTokenTokenizer(char[] input, char delim) {
		super(input, delim);
	}

	public StrHoldTokenTokenizer(char[] input, String delim) {
		super(input, delim);
	}

	public StrHoldTokenTokenizer(char[] input, StrMatcher delim,
			StrMatcher quote) {
		super(input, delim, quote);
	}

	public StrHoldTokenTokenizer(char[] input, StrMatcher delim) {
		super(input, delim);
	}

	public StrHoldTokenTokenizer(char[] input) {
		super(input);
	}

	public StrHoldTokenTokenizer(String input, char delim, char quote) {
		super(input, delim, quote);
	}

	public StrHoldTokenTokenizer(String input, char delim) {
		super(input, delim);
	}

	public StrHoldTokenTokenizer(String input, String delim) {
		super(input, delim);
	}

	public StrHoldTokenTokenizer(String input, StrMatcher delim,
			StrMatcher quote) {
		super(input, delim, quote);
	}

	public StrHoldTokenTokenizer(String input, StrMatcher delim) {
		super(input, delim);
	}

	public StrHoldTokenTokenizer(String input) {
		super(input);
	}

	@Override
	protected List tokenize(char[] chars, int offset, int count) {
		if (chars == null || count == 0) {
			return Collections.EMPTY_LIST;
		}
		StrBuilder buf = new StrBuilder();
		List tokens = new ArrayList();
		int pos = offset;

		// loop around the entire buffer
		while (pos >= 0 && pos < count) {
			// find next token
			pos = readNextToken(chars, pos, count, buf, tokens);

			// handle case where end of string is a delimiter
			if (pos >= count) {
				addToken(tokens, "");
			}
		}
		return tokens;
	}

	/**
	 * Adds a token to a list, paying attention to the parameters we've set.
	 * 
	 * @param list
	 *            the list to add to
	 * @param tok
	 *            the token to add
	 */
	private void addToken(List list, String tok) {
		if (tok == null || tok.length() == 0) {
			if (isIgnoreEmptyTokens()) {
				return;
			}
			if (isEmptyTokenAsNull()) {
				tok = null;
			}
		}
		list.add(tok);
	}

	/**
	 * Reads character by character through the String to get the next token.
	 * 
	 * @param chars
	 *            the character array being tokenized
	 * @param start
	 *            the first character of field
	 * @param len
	 *            the length of the character array being tokenized
	 * @param workArea
	 *            a temporary work area
	 * @param tokens
	 *            the list of parsed tokens
	 * @return the starting position of the next field (the character
	 *         immediately after the delimiter), or -1 if end of string found
	 */
	private int readNextToken(char[] chars, int start, int len,
			StrBuilder workArea, List tokens) {
		// skip all leading whitespace, unless it is the
		// field delimiter or the quote character
		while (start < len) {
			int removeLen = Math.max(getIgnoredMatcher().isMatch(chars, start,
					start, len), getTrimmerMatcher().isMatch(chars, start,
					start, len));
			if (removeLen == 0
					|| getDelimiterMatcher().isMatch(chars, start, start, len) > 0
					|| getQuoteMatcher().isMatch(chars, start, start, len) > 0) {
				break;
			}
			start += removeLen;
		}

		// handle reaching end
		if (start >= len) {
			addToken(tokens, "");
			return -1;
		}

		// handle empty token
		int delimLen = getDelimiterMatcher().isMatch(chars, start, start, len);
		if (delimLen > 0) {
			// **********************************************
			// * Hack this to add Match Token to token list *
			// **********************************************
			addToken(tokens, String.copyValueOf(chars, start, delimLen));
			addToken(tokens, "");
			return start + delimLen;
		}

		// handle found token
		int quoteLen = getQuoteMatcher().isMatch(chars, start, start, len);
		if (quoteLen > 0) {
			return readWithQuotes(chars, start + quoteLen, len, workArea,
					tokens, start, quoteLen);
		}
		return readWithQuotes(chars, start, len, workArea, tokens, 0, 0);
	}

	/**
	 * Reads a possibly quoted string token.
	 * 
	 * @param chars
	 *            the character array being tokenized
	 * @param start
	 *            the first character of field
	 * @param len
	 *            the length of the character array being tokenized
	 * @param workArea
	 *            a temporary work area
	 * @param tokens
	 *            the list of parsed tokens
	 * @param quoteStart
	 *            the start position of the matched quote, 0 if no quoting
	 * @param quoteLen
	 *            the length of the matched quote, 0 if no quoting
	 * @return the starting position of the next field (the character
	 *         immediately after the delimiter, or if end of string found, then
	 *         the length of string
	 */
	private int readWithQuotes(char[] chars, int start, int len,
			StrBuilder workArea, List tokens, int quoteStart, int quoteLen) {
		// Loop until we've found the end of the quoted
		// string or the end of the input
		workArea.clear();
		int pos = start;
		boolean quoting = (quoteLen > 0);
		int trimStart = 0;

		while (pos < len) {
			// quoting mode can occur several times throughout a string
			// we must switch between quoting and non-quoting until we
			// encounter a non-quoted delimiter, or end of string
			if (quoting) {
				// In quoting mode

				// If we've found a quote character, see if it's
				// followed by a second quote. If so, then we need
				// to actually put the quote character into the token
				// rather than end the token.
				if (isQuote(chars, pos, len, quoteStart, quoteLen)) {
					if (isQuote(chars, pos + quoteLen, len, quoteStart,
							quoteLen)) {
						// matched pair of quotes, thus an escaped quote
						workArea.append(chars, pos, quoteLen);
						pos += (quoteLen * 2);
						trimStart = workArea.size();
						continue;
					}

					// end of quoting
					quoting = false;
					pos += quoteLen;
					continue;
				}

				// copy regular character from inside quotes
				workArea.append(chars[pos++]);
				trimStart = workArea.size();

			} else {
				// Not in quoting mode

				// check for delimiter, and thus end of token
				int delimLen = getDelimiterMatcher().isMatch(chars, pos, start,
						len);
				if (delimLen > 0) {
					// return condition when end of token found
					addToken(tokens, workArea.substring(0, trimStart));

					// **********************************************
					// * Hack this to add Match Token to token list *
					// **********************************************
					addToken(tokens, String.copyValueOf(chars, pos, delimLen));
					return pos + delimLen;
				}

				// check for quote, and thus back into quoting mode
				if (quoteLen > 0) {
					if (isQuote(chars, pos, len, quoteStart, quoteLen)) {
						quoting = true;
						pos += quoteLen;
						continue;
					}
				}

				// check for ignored (outside quotes), and ignore
				int ignoredLen = getIgnoredMatcher().isMatch(chars, pos, start,
						len);
				if (ignoredLen > 0) {
					pos += ignoredLen;
					continue;
				}

				// check for trimmed character
				// don't yet know if its at the end, so copy to workArea
				// use trimStart to keep track of trim at the end
				int trimmedLen = getTrimmerMatcher().isMatch(chars, pos, start,
						len);
				if (trimmedLen > 0) {
					workArea.append(chars, pos, trimmedLen);
					pos += trimmedLen;
					continue;
				}

				// copy regular character from outside quotes
				workArea.append(chars[pos++]);
				trimStart = workArea.size();
			}
		}

		// return condition when end of string found
		addToken(tokens, workArea.substring(0, trimStart));
		return -1;
	}

	/**
	 * Checks if the characters at the index specified match the quote already
	 * matched in readNextToken().
	 * 
	 * @param chars
	 *            the character array being tokenized
	 * @param pos
	 *            the position to check for a quote
	 * @param len
	 *            the length of the character array being tokenized
	 * @param quoteStart
	 *            the start position of the matched quote, 0 if no quoting
	 * @param quoteLen
	 *            the length of the matched quote, 0 if no quoting
	 * @return true if a quote is matched
	 */
	private boolean isQuote(char[] chars, int pos, int len, int quoteStart,
			int quoteLen) {
		for (int i = 0; i < quoteLen; i++) {
			if ((pos + i) >= len || chars[pos + i] != chars[quoteStart + i]) {
				return false;
			}
		}
		return true;
	}
}
